import akshare as ak
import pandas as pd
from datetime import datetime
import re
import requests

def get_fund_portfolio(fund_code: str, year: int | str | None = None):
    """
    获取基金持仓信息（全部持仓，按最新报告期筛选由外部控制）
    :param fund_code: 基金代码，例如 "161725"（招商中证白酒）
    :param year: 年份，默认当前年份
    :return: DataFrame，仅包含最新报告期的持仓信息
    """
    if year is None:
        year = datetime.now().year

    df = ak.fund_portfolio_hold_em(symbol=fund_code, date=str(year))

    # 仅保留最新报告期的数据（akshare 会返回同一年内多个季度/报告期）
    period_col = None
    for col in ["报告期", "截止日期", "报告日期", "季度", "截止季度"]:
        if col in df.columns:
            period_col = col
            break

    if period_col is None:
        return df

    # 将报告期转为日期进行比较，失败则按字符串最大值兜底
    def _parse_period_series(series: pd.Series) -> pd.Series:
        mapping_quarter_to_month_day = {
            "一季报": (3, 31),
            "Q1": (3, 31),
            "中报": (6, 30),
            "半年度": (6, 30),
            "Q2": (6, 30),
            "三季报": (9, 30),
            "Q3": (9, 30),
            "年报": (12, 31),
            "年度": (12, 31),
            "Q4": (12, 31),
        }

        def _try_known_formats(x: str):
            if not isinstance(x, str):
                return pd.NaT
            x_strip = x.strip()
            # 处理中文季度，如 2025年一季报 / 2025年中报 / 2025年三季报 / 2025年年报
            if "年" in x_strip and any(k in x_strip for k in mapping_quarter_to_month_day.keys()):
                try:
                    year_text = x_strip.split("年")[0]
                    year_num = int(year_text)
                    for k, (m, d) in mapping_quarter_to_month_day.items():
                        if k in x_strip:
                            return datetime(year_num, m, d)
                except Exception:
                    return pd.NaT

            # 处理类似 2025Q1 / 2025-Q3
            if "Q" in x_strip:
                try:
                    year_num = int(x_strip[:4])
                    q = int(x_strip.split("Q")[-1][0])
                    md = {1: (3, 31), 2: (6, 30), 3: (9, 30), 4: (12, 31)}.get(q)
                    if md:
                        return datetime(year_num, md[0], md[1])
                except Exception:
                    return pd.NaT

            # 常见日期格式
            for fmt in [
                "%Y-%m-%d",
                "%Y/%m/%d",
                "%Y%m%d",
                "%Y-%m-%d %H:%M:%S",
                "%Y/%m/%d %H:%M:%S",
            ]:
                try:
                    return datetime.strptime(x_strip, fmt)
                except Exception:
                    continue
            return pd.NaT

        # 直接使用自定义解析，避免 pandas 推断格式产生告警
        parsed_list = series.apply(_try_known_formats)
        if parsed_list.notna().any():
            return parsed_list

        # 若全部失败，最后再兜底调用 to_datetime，并静默告警
        import warnings
        with warnings.catch_warnings():
            warnings.filterwarnings("ignore", category=UserWarning)
            direct = pd.to_datetime(series, errors="coerce")
        return direct

    periods = _parse_period_series(df[period_col])
    if periods.notna().any():
        latest_period = periods.max()
        latest_df = df.loc[periods == latest_period].copy()
    else:
        latest_value = df[period_col].astype(str).max()
        latest_df = df.loc[df[period_col].astype(str) == latest_value].copy()

    latest_df.reset_index(drop=True, inplace=True)
    return latest_df


def get_fund_latest_holdings(fund_code: str) -> pd.DataFrame:
    """
    获取指定基金“最新披露报告期”的全部持仓信息。
    逻辑：
    - 优先查询当前年份；若该年无数据（尚未披露），则回退到上一年。
    - 在命中的年份内，筛选最新报告期并返回其全部持仓行。

    :param fund_code: 基金代码，例如 "161725"
    :return: DataFrame，仅包含最新报告期的全部持仓信息
    """
    current_year = datetime.now().year

    df_current = get_fund_portfolio(fund_code, year=current_year)
    # 如果当前年查询结果为空，则回退到上一年
    if df_current is None or (hasattr(df_current, 'empty') and df_current.empty):
        df_prev = get_fund_portfolio(fund_code, year=current_year - 1)
        return df_prev
    return df_current


def get_fund_full_holdings(fund_code: str, max_years_back: int = 3) -> pd.DataFrame:
    """
    获取基金在最新“半年报/年报”披露期的“全部持仓”明细（非仅前十）。
    数据来源：东方财富 F10 档案异步接口（返回 HTML 片段），解析后组装为 DataFrame。

    - 依次尝试：当前年、往前逐年（最多 max_years_back 年），先 12 月（年报），再 6 月（半年报）。
    - 一旦获取到包含“股票投资明细”的表格即返回。

    :param fund_code: 基金代码（如 "018957"）
    :param max_years_back: 最多回溯的年份数
    :return: DataFrame，最新披露期的全部持仓；若未找到，返回空 DataFrame
    """
    session = requests.Session()
    headers = {
        "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0 Safari/537.36",
        "Referer": f"http://fundf10.eastmoney.com/jjjz_{fund_code}.html",
    }

    def _fetch_archives_html(year: int, month: int) -> str | None:
        # 东财 F10 档案异步接口：type=jjcc（基金持仓），topline 可拉更多条目
        url = (
            f"http://fundf10.eastmoney.com/FundArchivesDatas.aspx?"
            f"type=jjcc&code={fund_code}&topline=300&year={year}&month={month}&rt={int(datetime.now().timestamp())}"
        )
        resp = session.get(url, headers=headers, timeout=10)
        if resp.status_code != 200 or not resp.text:
            return None
        # 返回内容为 JavaScript 变量赋值，包含 apidata={content: '...html...'}
        m = re.search(r"apidata\s*=\s*\{[\s\S]*?\}\s*;?", resp.text)
        if not m:
            return None
        block = m.group(0)
        # 提取 content 字段中的 HTML 片段
        m2 = re.search(r"content:\s*'([\s\S]*?)'", block)
        if not m2:
            return None
        html = m2.group(1)
        # HTML 中可能包含转义的 \\ 或 \n
        html = html.encode('utf-8').decode('unicode_escape')
        return html

    def _parse_full_holdings_from_html(html: str) -> pd.DataFrame | None:
        if not html or '<table' not in html.lower():
            return None
        try:
            tables = pd.read_html(html)
        except Exception:
            return None
        if not tables:
            return None
        # 经验：包含“持仓明细/股票代码/股票名称/占净值比例”等字段的表才是目标
        for t in tables:
            cols = ''.join(map(str, t.columns))
            if any(k in cols for k in ["股票代码", "股票名称"]) and any(k in cols for k in ["占净值比例", "持股数", "持仓市值", "持仓数量"]):
                # 清理无关行
                df = t.copy()
                # 丢弃全空列
                df = df.dropna(axis=1, how='all')
                # 丢弃全空行
                df = df.dropna(axis=0, how='all')
                if not df.empty and df.shape[1] >= 3:
                    return df
        return None

    current_year = datetime.now().year
    for delta in range(0, max_years_back + 1):
        year = current_year - delta
        for month in [12, 6]:  # 年报优先，其次半年报
            html = _fetch_archives_html(year, month)
            if not html:
                continue
            df = _parse_full_holdings_from_html(html)
            if df is not None and not df.empty:
                return df

    return pd.DataFrame()

if __name__ == "__main__":
    # 示例：获取最新报告期“全部持仓”（半年报/年报）
    full_df = get_fund_full_holdings('018957')
    print(full_df)
    # ratio = portfolio.loc["300502", "占净值比例"]
    # print(ratio)

